/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

import fs, { Dirent, Stats } from 'fs';
import path from 'path';

import { EventType } from './event-type.js';
import { FsEvent } from './fs-event.js';

/**
 * 基于深度访问文件树, 不执行具体snapshot实例构建
 * 访问时返回对应FsEvent
 * Visitable实现类根据FsEvent处理snapshot实例的构建
 */
export class DirWalker {
  /**
   * 文件夹栈, 表示当前工作路径
   *
   * @private
   */
  private stack: Array<Directory> = [];
  private readonly followLinks: boolean;
  private readonly maxDepth: number;

  /**
   * 构造函数
   *
   * @param followLinks 是否支持文件链接
   * @param maxDepth 最大深度
   */
  constructor(followLinks: boolean, maxDepth?: number) {
    this.followLinks = followLinks;
    this.maxDepth = maxDepth ? maxDepth : Infinity;
  }

  /**
   * 入口函数
   *
   * @param file 文件绝对路径
   */
  walk(file: string) {
    let stats: Stats;
    try {
      stats = fs.lstatSync(file);
    } catch (e) {
      return undefined;
    }
    return this.visit(file, stats);
  }

  /**
   * 类迭代步进, 访问文件夹内元素
   */
  next(): FsEvent | undefined {
    if (this.stack.length < 1) {
      return undefined;
    }
    const top: Directory | undefined = this.peekLast();
    if (!top) {
      return undefined;
    }
    let ev: FsEvent | undefined;
    let entry: Dirent;

    // 迭代遍历当前工作文件夹下元素
    const iterator = top.iterator;
    do {
      const next: IteratorResult<Dirent> = iterator.next();
      if (next.done) {
        // 完成文件夹内元素遍历后抛出当前文件夹, 返回END_DIRECTORY事件
        this.stack.pop();
        return new FsEvent(EventType.END_DIRECTORY, top.name, top.absolutePath, top.stats);
      }
      entry = next.value;

      // 访问文件夹内元素
      const entryPath: string = path.resolve(top.absolutePath, entry.name);
      let stats: Stats;
      try {
        stats = fs.lstatSync(entryPath);
      } catch (e) {
        return undefined;
      }
      ev = this.visit(entryPath, stats);
    } while (ev === undefined);
    return ev;
  }

  /**
   * 处理文件及文件夹元素
   *
   * @param file 文件路径
   * @param stats fs.Stats
   * @private
   */
  private visit(file: string, stats: fs.Stats): FsEvent | undefined {
    // strategy of depth
    if (this.stack.length >= this.maxDepth) {
      return new FsEvent(EventType.ENTRY, path.basename(file), file, stats);
    }

    // strategy of link
    if (stats.isSymbolicLink()) {
      if (!this.followLinks) {
        return new FsEvent(EventType.ENTRY, path.basename(file), file, stats);
      }
      const realPath = fs.readlinkSync(file);
      if (this.wouldLoop(realPath)) {
        return new FsEvent(EventType.ENTRY, path.basename(file), file, stats);
      }
      file = realPath;
      stats = fs.lstatSync(realPath);
    }

    if (!stats.isDirectory()) {
      return new FsEvent(EventType.ENTRY, path.basename(file), file, stats);
    }

    // 处理文件夹元素, 构造文件夹对象到stack, 返回START_DIRECTORY
    const dirents = fs.readdirSync(file, { withFileTypes: true });
    const directory = new Directory(path.basename(file), file, stats, dirents[Symbol.iterator]());
    this.stack.push(directory);
    return new FsEvent(EventType.START_DIRECTORY, path.basename(file), file, stats);
  }

  private wouldLoop(dir: string): boolean {
    return -1 !== this.stack.findIndex((directory) => directory.absolutePath === dir);
  }

  private peekLast(): Directory | undefined {
    const length = this.stack.length;
    if (length < 1) {
      return undefined;
    }
    return this.stack[length - 1];
  }
}

class Directory {
  private readonly _name: string;
  private readonly _absolutePath: string;
  private readonly _iterator: IterableIterator<fs.Dirent>;
  private readonly _stats: Stats;

  constructor(name: string, absolutePath: string, stats: Stats, iterator: IterableIterator<fs.Dirent>) {
    this._name = name;
    this._absolutePath = absolutePath;
    this._stats = stats;
    this._iterator = iterator;
  }

  get name(): string {
    return this._name;
  }

  get absolutePath(): string {
    return this._absolutePath;
  }

  get iterator(): IterableIterator<fs.Dirent> {
    return this._iterator;
  }

  get stats(): Stats {
    return this._stats;
  }
}
